package org.jeecg.modules.system.controller.login;

import cn.hutool.core.util.RandomUtil;
import com.alibaba.fastjson.JSONObject;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.constant.SymbolConstant;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.login.LoginService;
import org.jeecg.common.login.parameter.DoubleFactorAuthParameter;
import org.jeecg.common.login.parameter.DoubleFactorExecuteParameter;
import org.jeecg.common.login.parameter.OriginalParameter;
import org.jeecg.common.message.MessageItem;
import org.jeecg.common.message.UnicomSmsUtil;
import org.jeecg.common.system.util.JwtUtil;
import org.jeecg.common.system.vo.DictModel;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.Md5Util;
import org.jeecg.common.util.PasswordUtil;
import org.jeecg.common.util.RedisUtil;
import org.jeecg.config.thirdapp.ThirdAppConfig;
import org.jeecg.modules.base.service.BaseCommonService;
import org.jeecg.modules.system.controller.login.constant.Prompt;
import org.jeecg.modules.system.controller.login.constant.SmsTemplate;
import org.jeecg.modules.system.entity.SysDepart;
import org.jeecg.modules.system.entity.SysTenant;
import org.jeecg.modules.system.entity.SysThirdAccount;
import org.jeecg.modules.system.entity.SysUser;
import org.jeecg.modules.system.service.*;
import org.jeecg.modules.system.util.RandImageUtil;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.http.HttpStatus;

import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * 登录通用处理类
 *
 * @author tianyi.jiang
 * @since 1.0.0
 */
@Slf4j
public class LoginHandler {

    protected static final String BASE_CHECK_CODES = "aBcDeFgHiJkLmNOPqrSTuvWXyzAbCdEfGhIjKlMnoPQrsTUvwXYz1234567890";

    @Value("${basicplatform.login.signature-secret:}")
    private String signatureSecret;

    protected final RedisUtil redisUtil;

    protected final UnicomSmsUtil unicomSmsUtil;

    protected final RsaEncryptProperty rsaEncryptProperty;

    protected final BaseCommonService baseCommonService;

    protected final ISysDictService sysDictService;

    protected final ISysTenantService sysTenantService;

    protected final ISysUserService sysUserService;

    protected final ISysDepartService sysDepartService;

    protected RedisTemplate<String, Object> redisTemplate;

    protected Result<?> routineVerification(OriginalParameter originalParameter) {
        String lowerCaseCaptcha = originalParameter.getCaptcha().toLowerCase();
        // 生成图形验证码Redis Key
        String captchaRedisKey = this.generateGraphicCodeRedisKey(lowerCaseCaptcha, originalParameter.getCheckKey());
        // 校验图形验证码
        if (this.verifyGraphicCode(captchaRedisKey, lowerCaseCaptcha)) {
            return Result.error(HttpStatus.PRECONDITION_FAILED.value(), Prompt.GRAPHIC_CODE_WRONG_PROMPT);
        }

        String username = originalParameter.getUsername();
        LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(SysUser::getUsername, username);
        SysUser sysUser = sysUserService.getOne(queryWrapper);
        // 校验用户是否有效
        Result<?> result = sysUserService.checkUserIsEffective(sysUser);
        if (!result.isSuccess()) {
            return result;
        }
        // 暗文密码解密
        String password = rsaEncryptProperty.decrypt(originalParameter.getPassword());
        // 校验用户名或密码是否正确
        String encryptPassword = PasswordUtil.encrypt(username, password, sysUser.getSalt());
        String correctPassword = sysUser.getPassword();
        if (!correctPassword.equals(encryptPassword)) {
            return Result.error(Prompt.USERNAME_OR_PASSWORD_WRONG_PROMPT);
        }
        return Result.OK(sysUser);
    }

    /**
     * 执行发送短信验证码
     *
     * @param mobile 明文手机号码
     * @author jiangtianyi
     * @since 1.0
     */
    protected Result<String> doSendSmsCode(String mobile) {
        // 生成图形验证码Redis Key
        String captchaRedisKey = this.generateSmsCodeRedisKey(mobile);
        // 判定电话号码是否发送过，短信验证码10分钟内有效
        Object object = redisUtil.get(captchaRedisKey);
        if (object != null) {
            return Result.error(CommonConstant.SC_OK_200, Prompt.SMS_CODE_EFFECTIVE_PROMPT);
        }
        // 短信验证码（6随机数）
        String captcha = RandomUtil.randomNumbers(6);
        // 短信内容
        String content = String.format(SmsTemplate.LOGIN_SMS_TEMPLATE, captcha);
        MessageItem messageItem = MessageItem.builder().customerMessageId(UUID.randomUUID().toString()).messageTo(mobile).messageContent(content).build();
        // 向"联通消息平台"发送短信
        if (unicomSmsUtil.sendSms(Collections.singletonList(messageItem))) {
            //验证码10分钟内有效
            redisUtil.set(captchaRedisKey, captcha, 600);
            return Result.OK(Prompt.SMS_CODE_SEND_SUCCESS_PROMPT);
        }
        return Result.error(Prompt.SMS_CODE_SEND_FAIL_PROMPT);
    }

    /**
     * 生成短信验证码Redis Key
     *
     * @param mobile 明文手机号码
     * @author jiangtianyi
     * @since 1.0
     */
    protected String generateSmsCodeRedisKey(String mobile) {
        String origin = CommonConstant.PHONE_REDIS_KEY_PRE + mobile + signatureSecret;
        return Md5Util.md5Encode(origin, StandardCharsets.UTF_8.toString());
    }

    /**
     * 执行登录
     *
     * @param sysUser {@link SysUser}
     * @return 用户角色、权限、部门等信息
     * @author jiangtianyi
     * @since 1.0
     */
    protected Result<JSONObject> doLogin(SysUser sysUser) {
        JSONObject jsonObject = new JSONObject(new LinkedHashMap<>());
        // 租户信息
        String tenantIds = sysUser.getRelTenantIds();
        if (StringUtils.isNotEmpty(tenantIds)) {
            List<Integer> tenantIdList = Stream.of(tenantIds.split(SymbolConstant.COMMA)).map(Integer::valueOf).collect(Collectors.toList());
            // 该方法仅查询有效的租户，若空说明所有的租户均无效。
            List<SysTenant> tenantList = sysTenantService.queryEffectiveTenant(tenantIdList);
            if (CollectionUtils.isEmpty(tenantList)) {
                return Result.error(Prompt.ALL_TENANT_FREEZE_PROMPT);
            }
            jsonObject.put("tenantList", tenantList);
        }

        // 部门信息
        List<SysDepart> departList = sysDepartService.queryUserDeparts(sysUser.getId());
        if (CollectionUtils.isEmpty(departList)) {
            jsonObject.put("multi_depart", 0);
        } else if (departList.size() == 1) {
            sysUserService.updateUserDepart(sysUser.getUsername(), departList.get(0).getOrgCode());
            // 返回用户登录部门org_code
            sysUser.setOrgCode(departList.get(0).getOrgCode());
            jsonObject.put("multi_depart", 1);
        } else {
            SysUser sysUserById = sysUserService.getById(sysUser.getId());
            if (StringUtils.isEmpty(sysUserById.getOrgCode())) {
                sysUserService.updateUserDepart(sysUser.getUsername(), departList.get(0).getOrgCode());
                // 返回用户登录部门org_code
                sysUser.setOrgCode(departList.get(0).getOrgCode());
            }
            // 如果用戶为选择部门，数据库为存在上一次登录部门，则取一条存进去
            jsonObject.put("multi_depart", 2);
        }
        jsonObject.put("departs", departList);

        // 字典
        Map<String, List<DictModel>> sysAllDictItems = sysDictService.queryAllDictItems();
        jsonObject.put("sysAllDictItems", sysAllDictItems);

        // 用户信息
        jsonObject.put("userInfo", sysUser);

        // 生成token
        String token = JwtUtil.sign(sysUser.getUsername(), sysUser.getPassword());
        // 设置token缓存有效时间
        redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, token, JwtUtil.EXPIRE_TIME * 2 / 1000);
        jsonObject.put("token", token);

        LoginUser loginUser = new LoginUser();
        BeanUtils.copyProperties(sysUser, loginUser);
        baseCommonService.addLog("用户名: " + loginUser.getUsername() + ",登录成功！", CommonConstant.LOG_TYPE_1, null, loginUser);
        return Result.OK(Prompt.LOGIN_SUCCESS_PROMPT, jsonObject);
    }

    /**
     * 生成普通图形验证码, 例如：aB34
     *
     * @param checkKey 验证码Key
     * @return 图片（base64）
     * @author jiangtianyi
     * @since 1.0
     */
    @SneakyThrows
    protected String graphicCodeBasic(String checkKey) {
        // 图形验证码
        String captcha = RandomUtil.randomString(BASE_CHECK_CODES, 4);
        String lowerCaseCaptcha = captcha.toLowerCase();
        // 生成图形验证码Redis Key
        String captchaRedisKey = this.generateGraphicCodeRedisKey(captcha, checkKey);
        // 图形验证码存于Redis中，且有效时长60秒
        redisUtil.set(captchaRedisKey, lowerCaseCaptcha, 60);
        log.debug("获取验证码，Redis key = {}，checkCode = {}", captchaRedisKey, captcha);
        return RandImageUtil.generate(captcha);
    }

    /**
     * 生成计算公式验证码, 例如：9 * 7 = ？
     *
     * @param checkKey 验证码Key
     * @return 图片（base64）
     * @author jiangtianyi
     * @since 1.2
     */
    @SneakyThrows
    protected String graphicCodeFormula(String checkKey) {
        int x = new Random().nextInt(9);
        int y = new Random().nextInt(9);
        int operator = new Random().nextInt(3) % 3;

        int value = 0;
        StringBuilder formula = new StringBuilder();

        switch (operator) {
            case 0:
                value = x + y;
                formula.append(x).append("+").append(y);
                break;
            case 1:
                value = x - y;
                formula.append(x).append("-").append(y);
                break;
            case 2:
                value = x * y;
                formula.append(x).append("x").append(y);
                break;
            default:
        }

        // 图形验证码
        String captcha = String.valueOf(value);
        String lowerCaseCaptcha = captcha.toLowerCase();
        // 生成图形验证码Redis Key
        String captchaRedisKey = this.generateGraphicCodeRedisKey(captcha, checkKey);
        // 图形验证码存于Redis中，且有效时长60秒
        redisUtil.set(captchaRedisKey, lowerCaseCaptcha, 60);

        log.debug("获取验证码，Redis key = {}，checkCode = {}", captchaRedisKey, captcha);
        return RandImageUtil.generate(formula.append("=").append("?").toString());
    }

    /**
     * 生成图形验证码Redis Key
     *
     * @param lowerCaseCaptcha 图形验证码(小写)
     * @param checkKey         验证码Key
     * @author jiangtianyi
     * @since 1.0
     */
    protected String generateGraphicCodeRedisKey(String lowerCaseCaptcha, String checkKey) {
        // 加入密钥作为混淆，避免简单的拼接，被外部利用，用户自定义该密钥即可
        String origin = lowerCaseCaptcha + checkKey + signatureSecret;
        return Md5Util.md5Encode(origin, StandardCharsets.UTF_8.toString());
    }

    /**
     * 校验验证码
     *
     * @param captchaRedisKey  验证码Redis Key
     * @param lowerCaseCaptcha 验证码(小写)
     * @return true:不匹配 / false:匹配
     * @author jiangtianyi
     * @since 1.0
     */
    protected boolean verifyGraphicCode(String captchaRedisKey, String lowerCaseCaptcha) {
        // 指定 lua 脚本
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        // 执行 lua 脚本
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
        Long result = redisTemplate.execute(redisScript, Collections.singletonList(captchaRedisKey), lowerCaseCaptcha);
        return result == null || result == 0L;
    }

    public LoginHandler(RedisUtil redisUtil, UnicomSmsUtil unicomSmsUtil, RsaEncryptProperty rsaEncryptProperty, BaseCommonService baseCommonService, ISysDictService sysDictService, ISysTenantService sysTenantService, ISysUserService sysUserService, ISysDepartService sysDepartService, RedisTemplate<String, Object> redisTemplate) {
        this.redisUtil = redisUtil;
        this.unicomSmsUtil = unicomSmsUtil;
        this.rsaEncryptProperty = rsaEncryptProperty;
        this.baseCommonService = baseCommonService;
        this.sysDictService = sysDictService;
        this.sysTenantService = sysTenantService;
        this.sysUserService = sysUserService;
        this.sysDepartService = sysDepartService;
        this.redisTemplate = redisTemplate;
    }
}